/**
 * Copyright (C) 2008 aileron.cc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package cc.aileron.commons.magic;

import static cc.aileron.commons.utility.Quote.quote;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;

import javassist.CannotCompileException;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;
import javassist.NotFoundException;

/**
 * implement class 
 * @author Aileron
 *
 */
public class MagicMethodClassCompilerImpl implements MagicMethodClassCompiler 
{
	
	/* (非 Javadoc)
	 * @see cc.aileron.commons.magic.MagicMethodClassCompiler#createImplementInstance(cc.aileron.commons.magic.MagicMethod, java.lang.Class)
	 */
	@SuppressWarnings("unchecked")
	public <T> T createImplementInstance(
		final MagicMethod command,
		final Class<T> interfaceClass)
	throws Exception
	{
		final String className = interfaceClass.getName();
		final CtClass targetClass = setConstruct(newClass(className));
		for(final CtMethod method: targetClass.getMethods())
		{
			if(!methodName.containsKey(method.getName()))
			{
				setMethod(className, targetClass, method);
			}
		}
		return (T) newInstance( 
				targetClass.toClass( interfaceClass.getClassLoader(), interfaceClass.getProtectionDomain() )
				, command);
	}
	
	/**
	 * new instance
	 * @param targetClass
	 * @param command
	 * @return
	 * @throws NoSuchMethodException
	 * @throws InstantiationException
	 * @throws InvocationTargetException
	 * @throws IllegalAccessException
	 */
	private Object newInstance(
		final Class<?> targetClass, final MagicMethod command)
	throws NoSuchMethodException
	,InstantiationException
	,InvocationTargetException
	,IllegalAccessException
	{
		final Constructor<?> constructor = 
			targetClass.getConstructor(MagicMethod.class);
		
		final Object result = 
			constructor.newInstance(new Object[]{ command });
		
		return result;
	}
	
	/**
	 * method の 設定
	 * @param targetClass
	 * @param interfaceMethod
	 * @throws NotFoundException
	 * @throws CannotCompileException
	 */
	private void setMethod(
			final String interfaceName
			, final CtClass targetClass
			, final CtMethod interfaceMethod)
	throws NotFoundException,CannotCompileException
	{
		final CtMethod targetMethod = 
			new CtMethod(
				interfaceMethod.getReturnType(), 
				interfaceMethod.getName(), 
				interfaceMethod.getParameterTypes(), targetClass);
		
		final String parameter = 
			"new " + MagicMethodParameter.class.getName() + 
			"(Class.forName(" + quote(interfaceName) + ")," +
				quote(interfaceMethod.getName()) + "," + 
				"$type,$sig" +
			")" ;
		
		final String method = interfaceMethod.getParameterTypes().length==0 
			? "this.command.execute("+parameter+",null);" 
			: "this.command.execute("+parameter+",$args);";
		
		final String body = interfaceMethod.getReturnType() == CtClass.voidType
		? "{"+method+"}"
		: "{"+"return ($r) "+method+"}";
		
		targetMethod.setBody( body );
		targetClass.addMethod(targetMethod);
	}
	
	/**
	 * 同じクラス名が被らない用にする為のキー値
	 */
	private static int i = 0;
	
	/**
	 * intarface を実装した class　を返します
	 * @param className
	 * @return
	 * @throws NotFoundException
	 */
	private CtClass newClass(final String className)
	throws NotFoundException
	{
	    final String ctClassName = "MAGIC_METHOD_" + (i++) + "_" + className.replace('.', '_');
		final CtClass targetClass = pool.makeClass(ctClassName);
		targetClass.addInterface( pool.get(className) );
		return targetClass;
	}
	
	/**
	 * コンストラクタを設定します
	 * @param targetClass
	 * @throws CannotCompileException
	 */
	private CtClass setConstruct(final CtClass targetClass)
	throws CannotCompileException,NotFoundException
	{
		setField(targetClass);
		
		final CtConstructor constructor =
			new CtConstructor(CONSTRUCT_ARGS, targetClass);
		constructor.setBody("this.command = $1;");
		targetClass.addConstructor(constructor);
		return targetClass;
	}
	
	/**
	 * field の追加
	 * @param targetClass
	 * @return
	 * @throws NotFoundException
	 * @throws CannotCompileException
	 */
	private CtClass setField(final CtClass targetClass)
	throws NotFoundException,CannotCompileException
	{ 	
		final CtField ctField = 
			new CtField(CLASS_COMMAND,CLASS_COMMAND_NAME, targetClass);
		targetClass.addField(ctField);
		return targetClass;
	}
	
	/**
	 * pool
	 */
	private static final ClassPool pool = ClassPool.getDefault();
	
	/**
	 * command name
	 */
	private static final String CLASS_COMMAND_NAME = "command";
	
	/**
	 * magicCommand class
	 */
	private static final CtClass CLASS_COMMAND;
	static
	{
		CtClass ctClass = null;
		try
		{
		    pool.appendClassPath(new ClassClassPath(MagicMethod.class));
			ctClass = pool.get(MagicMethod.class.getName());
		}
		catch(NotFoundException e)
		{
		    throw new RuntimeException(e);
		}
		CLASS_COMMAND = ctClass;
	}
	
	/**
	 * construct args
	 */
	private static final CtClass[] CONSTRUCT_ARGS = { CLASS_COMMAND };
	
	/**
	 * object.class の method 名 一覧
	 */
	private static final HashMap<String,Void> methodName = new HashMap<String, Void>();
	static
	{
		for(final Method method : Object.class.getMethods())
		{
			methodName.put(method.getName(), null);
		}
	}
}
